package de.thm.exa.algos;

import de.thm.exa.exceptions.HashException;
import de.thm.exa.helpers.Debugger;
import de.thm.exa.helpers.FileWriter;
import de.thm.exa.helpers.Hash;
import de.thm.exa.helpers.Timer;
import de.thm.exa.holders.Keyword;
import de.thm.exa.holders.Page;
import de.thm.exa.holders.PageList;
import de.thm.exa.holders.PageMap;
import de.thm.exa.holders.TestResult;
import de.thm.exa.interfaces.HashAlgorithm;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;

public class CuckooHash implements HashAlgorithm
{
    public static final int MAX_RECURSIONS = 20;
    
    private int currentRecursions = 0;
    private int rehashes = 0;
    
    private long elapsedTime = 0;
    
    private PageMap list1, list2;
    
    private int hashFunctionA = 0;
    private int hashFunctionB = 0;
    
    private int iteration = 0;
    
    private boolean succeeded = true;
    private boolean rehash = true;
    
    private ArrayList<Integer> oldFunctions1, oldFunctions2;
    
    public CuckooHash(int flag1, int flag2, boolean rehash)
    {
        this.rehash = rehash;
        
        list1 = new PageMap();
        list2 = new PageMap();
        
        hashFunctionA = flag1;
        hashFunctionB = flag2;
        
        oldFunctions1 = new ArrayList<>();
        oldFunctions2 = new ArrayList<>();
        
        try
        {
            generateHashFunctions();
        } catch(HashException e) {
            System.out.println("Error initializing CuckooHash! Shouldn't happen");
        }
    }
    
    @Override
    public void insert(Page page) throws HashException
    {
        Timer.start();
        for( Keyword activeKeyword : page.getKeywords() )
        {
            insert(activeKeyword, page);
        }
        elapsedTime = Timer.stop();
    }
    
    private void insert(Keyword keyword, Page page) throws HashException
    {
        // Insertion
        long key1 = Hash.generate(hashFunctionA, keyword);
        if( !list1.containsKey(key1) || list1.get(key1).getKeyword().equals(keyword) )
        {
            list1.put(keyword, key1, page);
        } else {
            if(Debugger.COUNT_COLLISIONS) { list1.collided(); }
            
            long key2 = Hash.generate(hashFunctionB, keyword);
            if( !list2.containsKey(key2) || list2.get(key2).getKeyword().equals(keyword) )
            {
                list2.put(keyword, key2, page);
            } else {
            	if(Debugger.COUNT_COLLISIONS) { list2.collided(); }
            
                PageList oldPageList = list1.get(key1);
                list1.put(keyword, key1, page);
                
                moveListBack(keyword, oldPageList);
            }
        }
    }
    
    private void moveListBack(Keyword keyword, PageList pageList) throws HashException
    {
        currentRecursions++;
        
        // Check abort / rehash
        if( currentRecursions > MAX_RECURSIONS )
        {
            currentRecursions = 0;
            
            if(rehash)
            {
                rehash();
            } else {
                throw new HashException("Functions don't work together");
            }
        }
        
        long key2 = Hash.generate(hashFunctionB, keyword);
        if( !list2.containsKey(key2) || list2.get(key2).getKeyword().equals(keyword) )
        {
            list2.put(keyword, key2, pageList);
        } else {
            if(Debugger.COUNT_COLLISIONS) { list2.collided(); }
            
            PageList oldPageList = list2.get(key2);
            list2.put(key2, pageList);

            moveListFwd(keyword, oldPageList);
        }
    }
    
    private void moveListFwd(Keyword keyword, PageList pageList) throws HashException
    {
        long key1 = Hash.generate(hashFunctionA, keyword);
        if( !list1.containsKey(key1) || list1.get(key1).getKeyword().equals(keyword) )
        {
            list1.put(keyword, key1, pageList);
        } else {
            if(Debugger.COUNT_COLLISIONS) { list1.collided(); }
            PageList oldPageList = list1.get(key1);
            list1.put(key1, pageList);

            moveListBack(keyword, oldPageList);
        }
    }
    
    private void rehash() throws HashException
    {
        rehashes++;
        
        generateHashFunctions();
        
        PageMap oldMap1 = (PageMap) list1.clone();
        PageMap oldMap2 = (PageMap) list2.clone();
        
        list1 = new PageMap();
        list2 = new PageMap();
        
        for( PageList pageList : oldMap1.values() )
        {
            moveListFwd(pageList.getKeyword(), pageList);
        }
        for( PageList pageList : oldMap2.values() )
        {
            moveListFwd(pageList.getKeyword(), pageList);
        }
    }
    
    private void generateHashFunctions() throws HashException
    {
        oldFunctions1.add( hashFunctionA % Hash.FUNCTION_COUNT );
        oldFunctions2.add( hashFunctionB % Hash.FUNCTION_COUNT );
        
        hashFunctionA++;
        hashFunctionB += (iteration+1);
        
        if( oldFunctions1.size() >= (Hash.FUNCTION_COUNT*Hash.FUNCTION_COUNT)-Hash.FUNCTION_COUNT )
        {
            throw new HashException("No function could assign every keyword!");
        } else if( oldFunctions1.size() >= Hash.FUNCTION_COUNT * (iteration+1) ) {
            iteration++;
        }
        
        System.out.println("Generated new functions: " + Hash.toString(hashFunctionA) + "," + Hash.toString(hashFunctionB));
    }
    
    @Override
    public PageList get(String keyword) throws HashException
    {
        long key1 = Hash.generate(hashFunctionA, keyword);
        if( list1.containsKey(key1) )
        {
            return list1.get(key1);
        } else {
            long key2 = Hash.generate(hashFunctionB, keyword);
            if( list2.containsKey(key2) )
            {
                return list2.get(key2);
            } else {
                throw new HashException("No such keyword defined!");
            }
        }
    }

    @Override
    public long getInserts() { return list1.getInserts() + list2.getInserts(); }

    @Override
    public long getCollisions() { return list1.getCollisions() + list2.getCollisions(); }
    
    @Override
    public long getElapsedTime() { return elapsedTime; }
    @Override
    public void setElapsedTime(long time) { this.elapsedTime = time; }
    
    @Override
    public void setSucceeded(boolean succeeded) { this.succeeded = succeeded; }
    @Override
    public boolean succeeded() { return succeeded; }
    
    @Override
    public String toString()
    {
        StringBuilder description = new StringBuilder("");
        description.append("\nAlgorithm Time: ").append(elapsedTime);
        description.append("\nCollisions:     ")
                   .append(list1.getCollisions() + list2.getCollisions());
        description.append("\nInserts:        ")
                   .append(list1.getInserts() + list2.getInserts());
        description.append("\nRehashes:       ")
                   .append(rehashes);
        description.append("\nSucceeded:      ")
                   .append(succeeded);
        
        if(succeeded)
        {
            description.append("\nFunctions:      ")
                       .append( Hash.toString(hashFunctionA) ).append(", ").append( Hash.toString(hashFunctionB) );
        }
        
        description.append("\nList 1:         ");
        if( list1.isEmpty() )
        {
            description.append("Empty\n");
        } else {
            description.append( list1.size() ).append(" Elements\n");
        }
        
        if(Debugger.LOGGING)
        {
            for( PageList pageList : list1.values() )
            {
                for(Page page : pageList)
                {
                    description.append( page.getWWW() ).append("\n")
                               .append( page.getHash() ).append("\n");
                    for( Keyword keyword : page.getKeywords() )
                    {
                        description.append("  ")
                                   .append(keyword.toString())
                                   .append("\n");
                    }
                }
            }
        }
        
        description.append("\nList 2:         ");
        if( list2.isEmpty() )
        {
            description.append("Empty\n");
        } else {
            description.append( list2.size() ).append(" Elements\n");
        }
        /*
        for( Page page : list2.values() )
        {
            description.append( page.getWWW() ).append("\n")
                       .append( page.getHash() ).append("\n");
            for( Keyword keyword : page.getKeywords() )
            {
                description.append("  ")
                           .append(keyword.toString())
                           .append("\n");
            }
        }
        */
        return description.toString();
    }
    
    @Override
    public void printFile(File file) throws IOException
    {
        String[] functions = new String[2];
        functions[0] = Hash.toString(hashFunctionA);
        functions[1] = Hash.toString(hashFunctionB);
        
        int[] listSizes = new int[2];
        listSizes[0] = list1.size();
        listSizes[1] = list2.size();
        
        FileWriter.write(
            file,
            new TestResult( "Cuckoo", elapsedTime, getCollisions(), getInserts(), rehashes, succeeded, functions, listSizes )
        );
    }
}
